﻿namespace Cellfish.Toolbox.Tasks
{
    using System;
    using System.Threading.Tasks;

    /// <summary>
    /// Extension methods to convert task methods to APM methods.
    /// </summary>
    public static class ToApmExtensions
    {
        /// <summary>
        /// Implements the APM pattern around a task.
        /// </summary>
        /// <param name="task">The asynchronous operation that should be converted into APM.</param>
        /// <returns>An IAsyncResult that represents the asynchronous operation, which could still be pending.</returns>
        /// <exception cref="System.ArgumentNullException">If task is null</exception>
        public static IAsyncResult ToApm(this Task task)
        {
            return task.ToApm(null, null);
        }

        /// <summary>
        /// Implements the APM pattern around a task.
        /// </summary>
        /// <param name="task">The asynchronous operation that should be converted into APM.</param>
        /// <param name="callback">An optional asynchronous callback, to be called when the write is complete.</param>
        /// <returns>An IAsyncResult that represents the asynchronous operation, which could still be pending.</returns>
        /// <exception cref="System.ArgumentNullException">If task is null</exception>
        public static IAsyncResult ToApm(this Task task, AsyncCallback callback)
        {
            return task.ToApm(callback, null);
        }

        /// <summary>
        /// Implements the APM pattern around a task.
        /// </summary>
        /// <param name="task">The asynchronous operation that should be converted into APM.</param>
        /// <param name="callback">An optional asynchronous callback, to be called when the write is complete.</param>
        /// <param name="state">A user-provided object that distinguishes this particular asynchronous write request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous operation, which could still be pending.</returns>
        /// <exception cref="System.ArgumentNullException">If task is null</exception>
        public static IAsyncResult ToApm(this Task task, AsyncCallback callback, object state)
        {
            if (task == null)
            {
                throw new ArgumentNullException("task");
            }

            return task.WithResult(true).ToApm(callback, state);
        }

        /// <summary>
        /// Implements the APM pattern around a task.
        /// </summary>
        /// <typeparam name="T">Result type of task.</typeparam>
        /// <param name="task">The asynchronous operation that should be converted into APM.</param>
        /// <returns>An IAsyncResult that represents the asynchronous operation, which could still be pending.</returns>
        /// <exception cref="System.ArgumentNullException">If task is null</exception>
        public static IAsyncResult ToApm<T>(this Task<T> task)
        {
            return task.ToApm(null, null);
        }

        /// <summary>
        /// Implements the APM pattern around a task.
        /// </summary>
        /// <typeparam name="T">Result type of task.</typeparam>
        /// <param name="task">The asynchronous operation that should be converted into APM.</param>
        /// <param name="callback">An optional asynchronous callback, to be called when the write is complete.</param>
        /// <returns>An IAsyncResult that represents the asynchronous operation, which could still be pending.</returns>
        /// <exception cref="System.ArgumentNullException">If task is null</exception>
        public static IAsyncResult ToApm<T>(this Task<T> task, AsyncCallback callback)
        {
            return task.ToApm(callback, null);
        }

        /// <summary>
        /// Implements the APM pattern around a task.
        /// </summary>
        /// <typeparam name="T">Result type of task.</typeparam>
        /// <param name="task">The asynchronous operation that should be converted into APM.</param>
        /// <param name="callback">An optional asynchronous callback, to be called when the write is complete.</param>
        /// <param name="state">A user-provided object that distinguishes this particular asynchronous write request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous operation, which could still be pending.</returns>
        /// <exception cref="System.ArgumentNullException">If task is null</exception>
        public static IAsyncResult ToApm<T>(this Task<T> task, AsyncCallback callback, object state)
        {
            if (task == null)
            {
                throw new ArgumentNullException("task");
            }

            if (task.AsyncState == state)
            {
                if (callback != null)
                {
                    task.ContinueWith(t => callback(task), TaskContinuationOptions.ExecuteSynchronously);
                }

                return task;
            }

            var tcs = new TaskCompletionSource<T>(state);
            task.ContinueWith(
                t =>
                    {
                        if (t.IsCanceled)
                        {
                            tcs.TrySetCanceled();
                        }
                        else if (t.IsFaulted)
                        {
                            tcs.TrySetException(t.Exception.InnerException);
                        }
                        else
                        {
                            tcs.TrySetResult(t.Result);
                        }

                        if (callback != null)
                        {
                            callback(tcs.Task);
                        }
                    }, 
                    TaskContinuationOptions.ExecuteSynchronously);
            return tcs.Task;
        }

        /// <summary>
        /// Ends an asynchronous operation.
        /// </summary>
        /// <typeparam name="T">The type of the asynchronous result.</typeparam>
        /// <param name="asyncResult">A reference to the outstanding asynchronous operation.</param>
        /// <returns>The result of the asynchrnous operation.</returns>
        /// <exception cref="System.ArgumentNullException">If asyncResult is null.</exception>
        /// <exception cref="System.ArgumentException">If asyncResult is not of type Task&lt;T&gt;.</exception>
        public static T EndTask<T>(this IAsyncResult asyncResult)
        {
            if (asyncResult == null)
            {
                throw new ArgumentNullException("asyncResult");
            }

            var task = asyncResult as Task<T>;
            if (task == null)
            {
                throw new ArgumentException("Argument is not a task of correct type.", "asyncResult");
            }

            try
            {
                return task.Result;
            }
            catch (AggregateException ae)
            {
                throw ae.Flatten().InnerException;
            }
        }

        /// <summary>
        /// Ends an asynchronous operation.
        /// </summary>
        /// <param name="asyncResult">A reference to the outstanding asynchronous operation.</param>
        /// <exception cref="System.ArgumentNullException">If asyncResult is null.</exception>
        /// <exception cref="System.ArgumentException">If asyncResult is not of type Task.</exception>
        public static void EndTask(this IAsyncResult asyncResult)
        {
            if (asyncResult == null)
            {
                throw new ArgumentNullException("asyncResult");
            }

            var task = asyncResult as Task;
            if (task == null)
            {
                throw new ArgumentException("Argument is not a task.", "asyncResult");
            }

            try
            {
                task.Wait();
            }
            catch (AggregateException ae)
            {
                throw ae.Flatten().InnerException;
            }
        }
    }
}
